/*
 * $Id: QEBand.m 44 2009-05-06 14:09:45Z jon@bway.net $
 * Sleeper
 *
 * Created by Jon Gordon on 4/22/09
 * Copyright Jon Gordon.  All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


#import <math.h>

#import "chalk.h"

#import "QEBand.h"

static NSDecimalNumber *decimalZero;

static NSMutableSet *allBands;
static NSMutableDictionary *allBandsByUid;
static NSMutableDictionary *allBandsByName;

static NSDictionary *bandCorrections;


@interface QEBand (private)
+ (QEBand *)createBandWithUid:(uint32_t)newUid
                         name:(NSString *)newName
                  lLimitAsStr:(NSString *)newLLimit
                  hLimitAsStr:(NSString *)newHLimit
             placeHolderAsStr:(NSString *)newPH;
+ (void)createAllBands;
@end


@implementation QEBand
+ (void)initialize
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    
    if (!initialized) {
        NSLog(@"  initializing");
        initialized = YES;
        decimalZero = [NSDecimalNumber zero];
        allBands = [[NSMutableSet alloc] init];
        allBandsByUid = [[NSMutableDictionary alloc] init];
        allBandsByName = [[NSMutableDictionary alloc] init];
        
        [QEBand createAllBands];
        
        // set up the list of band corrections--the key is a non-standard
        // band designation, and the value is the correct band name
        NSMutableDictionary *bc = [[NSMutableDictionary alloc] init];
        [bc setObject:@"2m"   forKey:@"SAT A"];
        [bc setObject:@"70cm" forKey:@"SAT B"];
        [bc setObject:@"2m"   forKey:@"SAT J"];
        [bc setObject:@"2m"   forKey:@"SAT JL"];
        [bc setObject:@"15m"  forKey:@"SAT K"];
        [bc setObject:@"15m"  forKey:@"SAT KT"];
        [bc setObject:@"24cm" forKey:@"SAT L"];
        [bc setObject:@"15m"  forKey:@"SAT R"];
        [bc setObject:@"2m"   forKey:@"SAT RA"];
        [bc setObject:@"15m"  forKey:@"SAT RK"];
        [bc setObject:@"15m"  forKey:@"SAT RT"];
        [bc setObject:@"70cm" forKey:@"SAT S"];
        [bc setObject:@"15m"  forKey:@"SAT T"];
        [bc setObject:@"70cm" forKey:@"SAT U/S"];
        [bc setObject:@"70cm" forKey:@"SAT U/V"];
        bandCorrections = [[NSDictionary alloc] initWithDictionary:bc];
        [bc release];
    } // if
} // initialize

+ (QEBand *)createBandWithUid:(uint32_t)newUid
                         name:(NSString *)newName
                  lLimitAsStr:(NSString *)newLLimit
                  hLimitAsStr:(NSString *)newHLimit
             placeHolderAsStr:(NSString *)newPH
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    // convert data types for comparison and initialization
    NSNumber *objUid = [NSNumber numberWithUnsignedLong:newUid];
    NSDecimalNumber *objLLimit = [NSDecimalNumber decimalNumberWithString:newLLimit];
    NSDecimalNumber *objHLimit = [NSDecimalNumber decimalNumberWithString:newHLimit];
    NSDecimalNumber *objPH = [NSDecimalNumber decimalNumberWithString:newPH];
    
    NSLog(@"  uid=%lu; name=\"%@\"; lLimit=\"%@\"; hLimit=\"%@\"; ph=\"%@\"",
          newUid, newName, newLLimit, newHLimit, newPH);
    NSLog(@"  >>uid=%@; lLimit=%@; hLimit=%@; ph=%@",
          objUid, objLLimit, objHLimit, objPH);
    
    QEBand *band;
    NSEnumerator *bandsEnum = [allBands objectEnumerator];
    while (nil != (band = [bandsEnum nextObject])) {
        // more integrity checks
        NSAssert2(![[band bandUid] isEqual:objUid],
                  @"  integrity check FAIL: UID %@ is already taken by band \"%@\"",
                  objUid, [band name]);
        NSAssert2(![[band name] isEqual:newName],
                  @"  integrity check FAIL: name \"%@\" is already taken by band with UID %@",
                  [band name], objUid);
        NSAssert2(([[band lLimit] compare:objHLimit] == NSOrderedAscending) ||
                  ([[band hLimit] compare:objLLimit] == NSOrderedAscending),
                  @"  integrity check FAIL: new band \"%@\" overlaps existing band \"%@\"",
                  newName, [band name]);
    } // while
    
    QEBand *result = [[QEBand alloc] initWithUid:objUid 
                                            name:newName 
                                          lLimit:objLLimit 
                                          hLimit:objHLimit 
                                     placeHolder:objPH];
    [allBands addObject:result];
    [allBandsByUid setObject:result forKey:objUid];
    [allBandsByName setObject:result forKey:[newName lowercaseString]];
    
    [result release]; // still retained in collections
    return result;
} // createBandWithUid:name:lLimitAsStr:hLimitAsStr:placeHolderAsStr:

- (id)init
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    // guaranteed to fail ASSERT
    return [self initWithUid:0 name:nil lLimit:nil hLimit:nil placeHolder:nil];
} // init

- (id)initWithUid:(uint32_t)newUid 
             name:(NSString *)newName 
      lLimitAsStr:(NSString *)newLLimit 
      hLimitAsStr:(NSString *)newHLimit 
 placeHolderAsStr:(NSString *)newPH
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    return [self initWithUid:[NSNumber numberWithUnsignedLong:newUid]
                        name:newName
                      lLimit:[NSDecimalNumber decimalNumberWithString:newLLimit]
                      hLimit:[NSDecimalNumber decimalNumberWithString:newHLimit]
                 placeHolder:[NSDecimalNumber decimalNumberWithString:newPH]];
} // initWithUid:name:lLimitAsStr:hLimitAsStr:placeHolder:


- (id)initWithUid:(NSNumber *)newUid 
             name:(NSString *)newName 
           lLimit:(NSDecimalNumber *)newLLimit 
           hLimit:(NSDecimalNumber *)newHLimit 
      placeHolder:(NSDecimalNumber *)newPH
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    // internal consistency checks - all params required
    NSAssert(nil != newUid, @" integrity check FAIL: nil UID");
    NSAssert(nil != newName, @" integrity check FAIL: nil band name");
    NSAssert(nil != newLLimit, @" integrity check FAIL: nil lower band limit");
    NSAssert(nil != newHLimit, @" integrity check FAIL: nil upper band limit");
    NSAssert(nil != newPH, @" integrity check FAIL: nil band placeholder");
    
    // legal values
    NSAssert([newUid unsignedLongValue] > 0, @" integrity check FAIL: zero UID");
    NSAssert1([newLLimit compare:decimalZero] == NSOrderedDescending,
              @"  integrity check FAIL: lower band limit (%@) is not positive", newLLimit);
    NSAssert1([newHLimit compare:decimalZero] == NSOrderedDescending,
              @"  integrity check FAIL: upper band limit (%@) is not positive", newLLimit);
    NSAssert1([newPH compare:decimalZero] == NSOrderedDescending,
              @"  integrity check FAIL: placeholder freq value (%@) is not positive", newLLimit);
    NSAssert2([newLLimit compare:newHLimit] != NSOrderedDescending,
              @"  integrity check FAIL: lower band limit (%@) exceeds upper limit (%@)", 
              newLLimit, newHLimit);
    /*
     * This next part requires 'splainin'.  In some cases the user (or ADIF file)
     * will specify only the band, not the frequency for the QSO.  In these
     * circumstances, we need a placeholder frequency to stick into the frequency
     * field.  Usually, the placeholder is the frequency at the bottom of the
     * band.  But for some bands, the placeholder is shortened for simplicity,
     * which takes it out of the band.  For example, the 33cm band is 902-904 MHz,
     * but it's sometimes referred to as 900 MHz.
     *
     * So we need to calculate alternate placeholders that are acceptable despite
     * not being technically within the allocated band.  One alternate placeholder
     * used here is the number with the same magnitude as the lower frequency limit,
     * but with the same most significant digit, all other digits being zero.
     *
     * The other is the largest integer that does not exceed the lower frequency
     * limit.
     *
     * TODO: REJECT ALT PLACEHOLDER WHEN IT IS IN ANOTHER BAND
     */
    double ll = [newLLimit doubleValue];
    double magnitude = floor(log10(ll)); // is exactly equal to an integer
    double powerOf10 = pow(10.0, magnitude); // is exactly equal to an integer
    double msd = floor(ll/powerOf10);
    NSDecimalNumber *altPH1 = [NSDecimalNumber decimalNumberWithMantissa:msd 
                                                                exponent:magnitude 
                                                              isNegative:NO];
    NSDecimalNumber *altPH2 = [NSDecimalNumber decimalNumberWithMantissa:floor(ll) 
                                                                exponent:0 
                                                              isNegative:NO];
    NSLog(@"  alternate placeholders: %@ and %@", altPH1, altPH2);
    if ([altPH1 isEqual:newPH] || [altPH2 isEqual:newPH]) {
        NSLog(@"  integrity check pass: alternate placeholder specified");
    } else {
        NSAssert2([newLLimit compare:newPH] != NSOrderedDescending,
                  @"  integrity check FAIL: placeholder (%@) is less than lower band limit (%@)", 
                  newPH, newLLimit);
        NSAssert2([newHLimit compare:newPH] != NSOrderedAscending,
                  @"  integrity check FAIL: placeholder (%@) exceeds upper limit (%@)", 
                  newPH, newHLimit);
    } // else
    
    self = [super init];
    if (nil != self) {
        bandUid = [newUid retain];
        name = [newName copy];
        lLimit = [newLLimit retain];
        hLimit = [newHLimit retain];
        placeHolder = [newPH retain];
    } // if
    return self;
} // initWithUid:name:lLimit:hLimit:placeHolder:

- (void)dealloc
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    [placeHolder release];
    [hLimit release];
    [lLimit release];
    [name release];
    [bandUid release];
    
    [super dealloc];
} // dealloc


+ (BOOL)isValidBandName:(NSString **)namePtr
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSLog(@"  entry: band name = \"%@\"", *namePtr);
    return [QEBand validateBand:namePtr error:nil];
    NSLog(@"   exit: band name = \"%@\"", *namePtr);
} // isValidBandName

+ (BOOL)validateBand:(id *)ioValue error:(NSError **)outError
{
    BOOL result = NO;
    
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSAssert(nil != ioValue, @"  validateBand passed nil pointer to band string");
    NSAssert(nil != *ioValue, @"  validateBand passed nil bandString");
    
    NSString *lwrName = [*ioValue lowercaseString];
    NSString *correctedBand = [bandCorrections objectForKey:lwrName];
    NSLog(@"  input band name: %@", *ioValue);
    NSLog(@"  lwrName: %@; correctedBand: %@", lwrName, correctedBand);
    if (nil == correctedBand) {
        correctedBand = lwrName;
    } else {
        NSLog(@"  replacing non-standard band name \"%@\" with standard name \"%@\"",
              *ioValue, correctedBand);
    } // else
    NSLog(@"  lwrName: %@; correctedBand: %@", lwrName, correctedBand);
    result = (nil != [QEBand bandForName:correctedBand]);
    if (result && ![correctedBand isEqual:(*ioValue)]) {
        *ioValue = correctedBand;
    } else if (!result && (nil != outError)) {
        NSArray *keys = [NSArray arrayWithObjects:NSLocalizedDescriptionKey,
                         NSLocalizedFailureReasonErrorKey,
                         NSLocalizedRecoverySuggestionErrorKey, nil];
        NSArray *objects = [NSArray arrayWithObjects:@"Unrecognized Band Name",
        [NSString stringWithFormat:@"\"%@\" does not identify any known band name.",
                            @"If you were importing an ADIF file, you can try to use a text editor to replace the non-standard band with a standard one."],
        nil];
        *outError = [[[NSError alloc] initWithDomain:QEChalkErrorDomain
                                                 code:QEBandUnknownNameError 
                                             userInfo:[NSDictionary dictionaryWithObjects:objects forKeys:keys]] autorelease];
    } // if
    
    return result;
} // validateBand:error:

+ (NSArray *)allBandsAsArray
{
    return [allBands allObjects];
} // allBandsAsArray

+ (QEBand *)bandForUid:(NSNumber *)uid
{
    return [allBandsByUid objectForKey:uid];
} // bandForUid:

+ (QEBand *)bandForName:(NSString *)bandName
{
    return [allBandsByName objectForKey:bandName];
} // bandForName:

+ (QEBand *)bandContainingFrequency:(NSDecimalNumber *)frequency
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSEnumerator *e = [allBands objectEnumerator];
    QEBand *nextBand;
    while (nil != (nextBand = [e nextObject])) {
        if ([[nextBand placeHolder] isEqual:frequency]) {
            return nextBand;
        } // if
        if (([[nextBand lLimit] compare:frequency] != NSOrderedDescending) &&
            ([[nextBand hLimit] compare:frequency] != NSOrderedAscending)) {
            return nextBand;
        } // if
    } // while
    
    return nil; // no match
} // bandContainingFrequency


- (NSNumber *)bandUid
{
    return [[bandUid retain] autorelease];
} // bandUid

- (NSString *)name
{
    return [[name retain] autorelease];
} // name

- (NSDecimalNumber *)lLimit
{
    return [[lLimit retain] autorelease];
} // name

- (NSDecimalNumber *)hLimit
{
    return [[hLimit retain] autorelease];
} // name

- (NSDecimalNumber *)placeHolder
{
    return [[placeHolder retain] autorelease];
} // name


+ (void)createAllBands
{
    [QEBand createBandWithUid:1  name:@"2190m"  lLimitAsStr:@".136"   hLimitAsStr:@".137"   placeHolderAsStr:@".136"];
    [QEBand createBandWithUid:2  name:@"560m"   lLimitAsStr:@".501"   hLimitAsStr:@".504"   placeHolderAsStr:@".500"];
    [QEBand createBandWithUid:3  name:@"160m"   lLimitAsStr:@"1.8"    hLimitAsStr:@"2.0"    placeHolderAsStr:@"1.8"];
    [QEBand createBandWithUid:4  name:@"80m"    lLimitAsStr:@"3.5"    hLimitAsStr:@"4.0"    placeHolderAsStr:@"3.5"];
    [QEBand createBandWithUid:5  name:@"60m"    lLimitAsStr:@"5.102"  hLimitAsStr:@"5.404"  placeHolderAsStr:@"5.102"];
    [QEBand createBandWithUid:6  name:@"40m"    lLimitAsStr:@"7.0"    hLimitAsStr:@"7.3"    placeHolderAsStr:@"7.0"];
    [QEBand createBandWithUid:7  name:@"30m"    lLimitAsStr:@"10.0"   hLimitAsStr:@"10.15"  placeHolderAsStr:@"10.0"];
    [QEBand createBandWithUid:8  name:@"20m"    lLimitAsStr:@"14.0"   hLimitAsStr:@"14.35"  placeHolderAsStr:@"14.0"];
    [QEBand createBandWithUid:9  name:@"17m"    lLimitAsStr:@"18.068" hLimitAsStr:@"18.168" placeHolderAsStr:@"18.0"];
    [QEBand createBandWithUid:10 name:@"15m"    lLimitAsStr:@"21.0"   hLimitAsStr:@"21.45"  placeHolderAsStr:@"21.0"];
    [QEBand createBandWithUid:11 name:@"12m"    lLimitAsStr:@"24.890" hLimitAsStr:@"24.99"  placeHolderAsStr:@"24.0"];
    [QEBand createBandWithUid:12 name:@"10m"    lLimitAsStr:@"28.0"   hLimitAsStr:@"29.7"   placeHolderAsStr:@"28.0"];
    [QEBand createBandWithUid:13 name:@"6m"     lLimitAsStr:@"50"     hLimitAsStr:@"54"     placeHolderAsStr:@"50"];
    [QEBand createBandWithUid:14 name:@"4m"     lLimitAsStr:@"70"     hLimitAsStr:@"71"     placeHolderAsStr:@"70"];
    [QEBand createBandWithUid:15 name:@"2m"     lLimitAsStr:@"144"    hLimitAsStr:@"148"    placeHolderAsStr:@"144"];
    [QEBand createBandWithUid:16 name:@"1.25m"  lLimitAsStr:@"222"    hLimitAsStr:@"225"    placeHolderAsStr:@"222"];
    [QEBand createBandWithUid:17 name:@"70cm"   lLimitAsStr:@"420"    hLimitAsStr:@"450"    placeHolderAsStr:@"420"];
    [QEBand createBandWithUid:18 name:@"33cm"   lLimitAsStr:@"902"    hLimitAsStr:@"928"    placeHolderAsStr:@"900"];
    [QEBand createBandWithUid:19 name:@"23cm"   lLimitAsStr:@"1240"   hLimitAsStr:@"1300"   placeHolderAsStr:@"1240"];
    [QEBand createBandWithUid:20 name:@"13cm"   lLimitAsStr:@"2300"   hLimitAsStr:@"2450"   placeHolderAsStr:@"2300"];
    [QEBand createBandWithUid:21 name:@"9cm"    lLimitAsStr:@"3300"   hLimitAsStr:@"3500"   placeHolderAsStr:@"3300"];
    [QEBand createBandWithUid:22 name:@"3cm"    lLimitAsStr:@"10000"  hLimitAsStr:@"10500"  placeHolderAsStr:@"10000"];
    [QEBand createBandWithUid:23 name:@"1.25cm" lLimitAsStr:@"24000"  hLimitAsStr:@"24250"  placeHolderAsStr:@"24000"];
    [QEBand createBandWithUid:24 name:@"6mm"    lLimitAsStr:@"47000"  hLimitAsStr:@"47200"  placeHolderAsStr:@"47000"];
    [QEBand createBandWithUid:25 name:@"4mm"    lLimitAsStr:@"75500"  hLimitAsStr:@"81000"  placeHolderAsStr:@"75500"];
    [QEBand createBandWithUid:26 name:@"2.5mm"  lLimitAsStr:@"119980" hLimitAsStr:@"120020" placeHolderAsStr:@"120000"];
    [QEBand createBandWithUid:27 name:@"2mm"    lLimitAsStr:@"142000" hLimitAsStr:@"149000" placeHolderAsStr:@"142000"];
    [QEBand createBandWithUid:28 name:@"1mm"    lLimitAsStr:@"241000" hLimitAsStr:@"250000" placeHolderAsStr:@"241000"];
} // createAllBands
@end
